package org.fastsyncer.plugin.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;

import org.apache.commons.lang.StringUtils;
import org.fastsyncer.common.constant.ConnectorConstant;
import org.fastsyncer.common.entity.LdapConfig;
import org.fastsyncer.common.entity.Mapping;
import org.fastsyncer.common.entity.MappingTask;
import org.fastsyncer.connector.ldap.LdapConnector;
import org.fastsyncer.connector.util.LdapUtil;
import org.fastsyncer.plugin.AbstractConvert;
import org.fastsyncer.plugin.impl.pool.DatabasePool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.ldap.AuthenticationException;
import org.springframework.ldap.CommunicationException;
import org.springframework.ldap.core.ContextMapper;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.LdapTemplate;

/**
 * <ul>
 * <dt>星盾Ldap目录树形结构转换规则：</dt>
 *  <dl>
 *      <li>根据组织的id和父节点id计算层级</li>
 *      <li>优先插入最顶层节点,然后是叶子节点...</li>
 *      <li>计算每条数据的层级dn,例如最顶层节点是ou=aostar,dc=com,子节点ou=xd,ou=aostar,dc=com</li>
 *  </dl>
 * </ul>
 * 
 * @author AE86
 * @date 2018年02月06日 下午14:08:12
 * @version 1.0.0
 */
public class AostarLdapAppOrgConvertImpl extends AbstractConvert {

    private final Logger logger = LoggerFactory.getLogger(AostarLdapAppOrgConvertImpl.class);

    private final String OU = "ou";
    
    private final String CN = "cn";

    /**
     * 应用组织ID
     */
    private final String ID = "ID";
    
    /**
     * 应用组织上级ID
     */
    private final String PARENT_ID = "PARENT_ID";
    
    /**
     * 应用组织名称
     */
    private final String ORG_NAME = "ORG_NAME";

    /**
     * 根据ID查询应用组织
     * <pre>SELECT ID,PARENT_ID,ORG_NAME FROM SD_APPORG WHERE IS_DELETED = '0' AND ID = ?</pre>
     */
    private final String SQL_QUERY_APPORG_BY_ID = "SELECT ID,PARENT_ID,ORG_NAME FROM SD_APPORG WHERE IS_DELETED = '0' AND ID = ?";
    
    @Override
    public String getConvertName() {
        return "应用组织_港中旅目录组织转换";
    }
    
    @Override
    public void convert(MappingTask task, List<Map<String, Object>> source, List<Map<String, Object>> target) {
        String dn = this.getDn(task);
        if (StringUtils.isBlank(dn)) {
            return;
        }

        // 转换为树形结构
        this.buildTree(dn, source, target);
    }

    @Override
    public void convert(MappingTask task, Map<String, Object> source, Map<String, Object> target, String event, Map<String, Object> beforeData) {
        String dn = this.getDn(task);
        if (StringUtils.isBlank(dn)) {
            return;
        }

        // 创建连接池
        DatabasePool pool = DatabasePool.getInstance();
        String uuid = pool.create(task.getSourceMapping());
        if (StringUtils.isBlank(uuid)) {
            return;
        }
        try {
            // 获取数据库连接
            JdbcTemplate jdbc = pool.get(uuid);
            if (null == jdbc) {
                return;
            }
            // 转换为树形结构
            this.buildTree(task, event, dn, source, target, jdbc, beforeData);
        } catch (Exception e) {
            logger.info(e.getClass() + " >> " + e.getLocalizedMessage());
        } finally {
            pool.close(uuid);
        }
    }
    
    /**
     * 获取目录的dn
     * @param task
     * @return
     */
    private String getDn(MappingTask task) {
        Mapping targetMapping = task.getTargetMapping();
        String connector = targetMapping.getConnector();
        if (!ConnectorConstant.LDAP_CONNECTOR.equals(connector)) {
            return null;
        }
        // 获取目录dn
        LdapConfig config = (LdapConfig) targetMapping.getConfig();
        return config.getDn();
    }

    /**
     * 构造树形结构
     * @param dn
     * @param source
     * @param target
     */
    private void buildTree(String dn, List<Map<String, Object>> source, List<Map<String, Object>> target) {
        // 获取总条数
        int sourceSize = source.size();
        int targetSize = target.size();
        if (sourceSize != targetSize) {
            return;
        }

        // 1.按照Id标记每一条数据
        Map<String, Map<String, Object>> kv = new HashMap<String, Map<String, Object>>(targetSize);
        Map<String, Integer> tOffset = new HashMap<String, Integer>(targetSize);
        // 根据ID快速定位每一条组织
        Map<String, Object> sNode = null;
        Object id;
        for (int i = 0; i < targetSize; i++) {
            sNode = source.get(i);
            id = sNode.get(ID);
            if (null == id) {
                continue;
            }
            kv.put(id.toString(), sNode);
            tOffset.put(id.toString(), i);
        }
        if(kv.isEmpty()){
            return;
        }

        // 2.计算ou层级并分区
        Map<Integer, BlockingQueue<Integer>> partition = new TreeMap<Integer, BlockingQueue<Integer>>();
        BlockingQueue<Integer> ids;
        Object[] pathAndLevelArr;
        StringBuilder path;
        int level;
        String pid;
        String name;
        Map<String, Object> node;
        Integer offsetValue;
        for (int i = 0; i < targetSize; i++) {
            sNode = source.get(i);
            id = String.valueOf(sNode.get(ID));
            pid = String.valueOf(sNode.get(PARENT_ID));
            name = String.valueOf(sNode.get(ORG_NAME));
            // ou=中电启明星
            path = new StringBuilder().append(dn).append("=").append(name);

            // 计算节点层级
            pathAndLevelArr = this.getPathAndLevel(dn, kv, path, pid, 0);
            if (null == pathAndLevelArr || 2 != pathAndLevelArr.length) {
                continue;
            }
            
            // 找到目标源
            offsetValue = tOffset.get(id);
            node = target.get(offsetValue);
            if (null != node) {
                path = (StringBuilder) pathAndLevelArr[0];
                // dn全路径
                node.put(OU, path);
            }

            // 获取层级分区
            level = (int) pathAndLevelArr[1];
            ids = partition.get(level);
            if (null == ids) {
                partition.put(level, new LinkedBlockingQueue<Integer>());
                ids = partition.get(level);
            }
            ids.offer(offsetValue);
        }

        // 3.根据分区装箱
        List<Map<String, Object>> res = new ArrayList<Map<String,Object>>(targetSize);
        // 按顺序插入目标源集合
        for (Map.Entry<Integer, BlockingQueue<Integer>> p : partition.entrySet()) {
            ids = p.getValue();
            if (null == ids) {
                continue;
            }
            while (!ids.isEmpty()) {
                offsetValue = ids.poll();
                node = target.get(offsetValue);
                if (null == node) {
                    continue;
                }
                // 根据分区顺序插入
                res.add(node);
            }
        }
        target.clear();
        target.addAll(res);
        
        // 标记对象为null表示可以被jvm回收
        res = null;
        partition = null;
        tOffset = null;
        kv = null;
    }

    /**
     * 构造树形结构
     * @param dn
     * @param source
     * @param target
     * @throws NamingException 
     * @throws CommunicationException 
     */
    private void buildTree(MappingTask task, String event, String dn, Map<String, Object> source, Map<String, Object> target, JdbcTemplate jdbc, Map<String, Object> beforeData) throws CommunicationException, NamingException {
        // 1.获取数据源父ID
        String pid = String.valueOf(source.get(PARENT_ID));

        // 2.获取节点基本信息
        String name = String.valueOf(target.get(CN));
        // 定义父节点dn全路径
        StringBuilder _dnParent = new StringBuilder();

        // 3.递归计算节点层级
        _dnParent = this.getPath(dn, _dnParent, pid, jdbc);
        
        // 4.拼接更新后的dn全路径,ou=org,dc=aostar,dc=com
        StringBuilder newDn = new StringBuilder();
        // ou=org
        newDn.append(dn).append("=").append(name);
        // dc=aostar,dc=com
        newDn.append(_dnParent);
        
        // 5.计算dn是否有变更
        // 由于港中旅的目录dn路径是用的组织名称,而组织名称有可能会发生变更,导致修改的数据无法找到原目录的dn路径
        // 故此,如果发生修改操作时,保证组织名称变化前后一致
        this.refeshDn(task, event, target, beforeData, dn, _dnParent, newDn);

        // 6.设置dn为节点全路径
        target.put(OU, newDn.toString());
    }

    /**
     * 更新变化后的dn路径
     * @param task
     * @param event
     * @param target
     * @param beforeData
     * @throws AuthenticationException 
     * @throws NamingException 
     * @throws CommunicationException 
     */
    private void refeshDn(MappingTask task, String event, Map<String, Object> target, Map<String, Object> beforeData, String dn, StringBuilder _dnParent, StringBuilder newDn) throws AuthenticationException, CommunicationException, NamingException {
        // 1.排除非更新操作
        if (!ConnectorConstant.OPERTION_UPDATE.equals(event)) {
            return;
        }

        // 2.排除非dn地址变化的操作
        // 更新前的名称
        String oldName = String.valueOf(beforeData.get(ORG_NAME));
        // 更新后的名称
        String newName = String.valueOf(target.get(CN));
        // 未修改组织名称
        if (oldName.equals(newName)) {
            return;
        }

        // 3.不一致,证明目录的dn地址发生变更,更新dn地址
        // 解析目标源配置
        Mapping targetMapping = task.getTargetMapping();
        LdapConfig c = (LdapConfig) targetMapping.getConfig();
        LdapTemplate ldapTemplate = LdapConnector.getInstance().getLdapTemplate(c);
        
        // 5.修改为最新的dn
        // 需求:修改某个节点名称
        this.reDnNameTree(ldapTemplate, _dnParent.toString(), dn, oldName, newName);
    }
    
    /**
     * 递归获取节点的dn全路径和层级
     * @param dn
     * @param kv
     * @param path
     * @param parentId
     * @param level
     * @return new Object[] { 层级, dn全路径 }
     */
    private Object[] getPathAndLevel(String dn, Map<String, Map<String, Object>> kv, StringBuilder path, String parentId, int level) {
        // 获取父节点
        Map<String, Object> node = kv.get(parentId);
        if (StringUtils.isBlank(parentId) || null == node) {
            return new Object[] { path, level };
        }
        String name = String.valueOf(node.get(ORG_NAME));
        String pid = String.valueOf(node.get(PARENT_ID));
        // ,ou=
        path.append(",").append(dn).append("=").append(name);
        return getPathAndLevel(dn, kv, path, pid, (level + 1));
    }

    /**
     * 递归获取节点的dn全路径
     * @param dn
     * @param path
     * @param pid
     * @return dn全路径
     */
    private StringBuilder getPath(String dn, StringBuilder path, String parentId, JdbcTemplate jdbc) {
        // 获取父节点
        Map<String, Object> node;
        try {
            node = jdbc.queryForMap(SQL_QUERY_APPORG_BY_ID, new Object[] { parentId });
        } catch (DataAccessException e) {
            node = null;
        }
        if (null == node) {
            return path;
        }
        String name = String.valueOf(node.get(ORG_NAME));
        String pid = String.valueOf(node.get(PARENT_ID));
        // ,ou=中电启明星
        path.append(",").append(dn).append("=").append(name);
        return getPath(dn, path, pid, jdbc);
    }

    private final String _dn = "_dn";
    
    /**
     * 递归修改某个节点名称
     * @param ldapTemplate
     * @param _dnParent 上级dn
     * @param dn dn属性
     * @param oldName 旧dn名称
     * @param newName 新dn名称
     */
    private void reDnNameTree(LdapTemplate ldapTemplate, String _dnParent, String dn, String oldName, String newName) {
        // 拼接旧dn,cn=ae86,ou=scdl,dc=aostar,dc=com
        String oldDn = new StringBuilder(dn).append("=").append(oldName).append(_dnParent).toString();
        
        // 1.获取修改的节点和所有子节点
        List<Map<String, Object>> oldNodes = this.queryTree(ldapTemplate, oldDn);
        if(null == oldNodes || oldNodes.isEmpty()){
            return;
        }
        
        // 2.重置dn名称
        this.reDnName(oldNodes, _dnParent, dn, newName);
        
        // 3.创建新节点和子节点
        int size = oldNodes.size();
        DirContextAdapter ctx = null;
        Map<String, Object> row = null;
        String k = null;
        Object v = null;
        for (int i = 0; i < size; i++) {
            row = oldNodes.get(i);
            // 3.1设置域 cn=ae86,ou=scdl,dc=aostar,dc=com
            // 在2.0版本推荐:DirContextAdapter是使用LDAP属性非常有用的工具，特别是添加或修改数据时。
            ctx = new DirContextAdapter(String.valueOf(row.get(_dn)));
            try {
                // 移除多余的属性
                row.remove(_dn);

                // 3.2转换基本属性名称和值类型
                for (Map.Entry<String, Object> obj : row.entrySet()) {
                    k = obj.getKey();
                    v = obj.getValue();
                    if(null == v){
                        continue;
                    }
                    // 如果是数组
                    if(v instanceof Object[]){
                        ctx.setAttributeValues(k, (Object[]) v, true);
                        continue;
                    }
                    // 目标源名称
                    ctx.addAttributeValue(k, String.valueOf(v));
                }

                // 3.3绑定
                ldapTemplate.bind(ctx);
            } catch (Exception e) {
                logger.error("", e);
            } finally {
                LdapUtil.closeContext(ctx);
            }
        }
        
        // 4.删除修改的节点和子节点
        ldapTemplate.unbind(oldDn, true);
        k = null;
        v = null;
        row = null;
        oldNodes = null;
    }
    
    /**
     * 重置dn名称
     * @param oldNodes 旧节点集合
     * @param _dnParent 上级dn
     * @param dn dn属性
     * @param orgName 新组织名称
     */
    private void reDnName(List<Map<String, Object>> oldNodes, String _dnParent, String dn, String orgName) {
        // ou=中电启明星666
        String node = new StringBuilder(dn).append("=").append(orgName).toString();
        boolean isExistParentDn = StringUtils.isNotBlank(_dnParent);
        
        Map<String, Object> row;
        Object _dn_f;
        String oldDn;
        String newDn = "";
        int size = oldNodes.size();
        for (int i = 0; i < size; i++) {
            row = oldNodes.get(i);
            _dn_f = row.get(_dn);
            if(null == _dn_f){
                continue;
            }
            // ou=test,ou=中电启明星,ou=中国旅游集团有限公司
            oldDn = String.valueOf(_dn_f);
            
            //************************************ 计算整颗树节点的新dn路径   -----start ************************************
            // 去掉上级节点
            if(isExistParentDn){
                // ou=test,ou=中电启明星,ou=中国旅游集团有限公司
                // ou=test,ou=中电启明星
                int lastIndexOf = oldDn.lastIndexOf(_dnParent);
                if(0 < lastIndexOf){
                    newDn = oldDn.substring(0, lastIndexOf);
                }
            }else{
                // ou=test,ou=中电启明星
                newDn = oldDn;
            }
            
            // 判断是否有层级,防止包含子节点,更新为新的dn
            if(newDn.contains(",")){
                // ou=test,ou=中电启明星
                // ou=test,ou=中电启明星666,ou=中国旅游集团有限公司
                newDn = new StringBuilder()
                        .append(newDn.substring(0, newDn.lastIndexOf(",") + 1))
                        .append(node)
                        .append(_dnParent).toString();
            }else{
                // ou=中电启明星666,ou=中国旅游集团有限公司
                newDn = new StringBuilder().append(node).append(_dnParent).toString();
            }
            //************************************ 计算整颗树节点的新dn路径  -----end **************************************
            // 修改为新dn
            // row.put("_dn", "ou=中电启明星666,ou=中国旅游集团有限公司");
            row.put(_dn, newDn);
            
            // 因为修改的节点名称是首条
            if(i==0){
                row.put("ou", orgName);
                row.put("cn", orgName);
                continue;
            }
        }
        
        newDn = null;
        oldDn = null;
        _dn_f = null;
        row = null;
    }

    /**
     * 查询指定dn下面所有节点
     * @param ldapTemplate
     * @param dn
     * @return
     */
    private List<Map<String, Object>> queryTree(LdapTemplate ldapTemplate, String dn) {
        if(StringUtils.isBlank(dn)){
            return null;
        }

        /**
         * 0:OBJECT_SCOPE,搜索指定的命名对象。 <br />
         * 1:ONELEVEL_SCOPE,只搜索指定命名对象的一个级别，这是缺省值。<br />
         * 2:SUBTREE_SCOPE,搜索以指定命名对象为根结点的整棵树,就像递归查询
         **/
        SearchControls controls = new SearchControls();
        controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
        
        ContextMapper<Map<String, Object>> contextMapper = new ContextMapper<Map<String, Object>>() {

            @Override
            public Map<String, Object> mapFromContext(Object ctx) throws javax.naming.NamingException {
                DirContextAdapter c = (DirContextAdapter) ctx;
                
                Attributes attrs = c.getAttributes();
                Map<String, Object> r = new HashMap<String, Object>();
                NamingEnumeration<String> iDs = attrs.getIDs();
                String key;
                Attribute attr;
                Object[] v;
                int size;
                while (iDs.hasMoreElements()) {
                    key = (String) iDs.nextElement();
                    attr = attrs.get(key);

                    size = attr.size();
                    // 单个值
                    if (1 == size) {
                        r.put(key, attr.get());
                        continue;
                    }

                    // 多个值
                    v = new Object[size];
                    for (int i = 0; i < size; i++) {
                        v[i] = attr.get(i);
                    }
                    r.put(key, v);
                }
                // 获取全路径dn
                r.put(_dn, c.getDn());
                return r;
            }
        };

        return ldapTemplate.search(dn.toString(), "(objectClass=*)", controls, contextMapper);
    }
    
}
